/* -*- Mode: Java; c-basic-offset: 4; tab-width: 4; indent-tabs-mode: nil; -*-
 * This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this
 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */

package org.mozilla.vrbrowser.browser

import android.content.Context
import android.graphics.BitmapFactory
import android.graphics.drawable.BitmapDrawable
import android.os.Handler
import android.os.Looper
import android.util.Log
import androidx.lifecycle.ProcessLifecycleOwner
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.future.future
import kotlinx.coroutines.launch
import mozilla.components.concept.sync.*
import mozilla.components.service.fxa.FirefoxAccount
import mozilla.components.service.fxa.SyncEngine
import mozilla.components.service.fxa.manager.SyncEnginesStorage
import mozilla.components.service.fxa.sync.SyncReason
import mozilla.components.service.fxa.sync.SyncStatusObserver
import mozilla.components.service.fxa.sync.getLastSynced
import org.mozilla.vrbrowser.R
import org.mozilla.vrbrowser.VRBrowserApplication
import org.mozilla.vrbrowser.telemetry.GleanMetricsService
import org.mozilla.vrbrowser.utils.BitmapCache
import org.mozilla.vrbrowser.utils.SystemUtils
import org.mozilla.vrbrowser.utils.ViewUtils
import java.net.URL
import java.util.concurrent.CompletableFuture
import kotlin.concurrent.thread

const val PROFILE_PICTURE_TAG = "fxa_profile_picture"

class Accounts constructor(val context: Context) {

    private val LOGTAG = SystemUtils.createLogtag(Accounts::class.java)

    enum class AccountStatus {
        SIGNED_IN,
        SIGNED_OUT,
        NEEDS_RECONNECT
    }

    enum class LoginOrigin {
        BOOKMARKS,
        HISTORY,
        SETTINGS,
        SEND_TABS,
        NONE
    }

    var profilePicture: BitmapDrawable? = loadDefaultProfilePicture()
    var loginOrigin: LoginOrigin = LoginOrigin.NONE
        private set
    var originSessionId: String? = null
        private set
    var accountStatus = AccountStatus.SIGNED_OUT
    private val accountListeners = ArrayList<AccountObserver>()
    private val syncListeners = ArrayList<SyncStatusObserver>()
    private val deviceConstellationListeners = ArrayList<DeviceConstellationObserver>()
    private val services = (context.applicationContext as VRBrowserApplication).services
    private var otherDevices = emptyList<Device>()
    private val syncStorage = SyncEnginesStorage(context)
    var isSyncing = false

    private val syncStatusObserver = object : SyncStatusObserver {
        override fun onStarted() {
            Log.d(LOGTAG, "Account syncing has started")

            isSyncing = true
            syncListeners.toMutableList().forEach {
                Handler(Looper.getMainLooper()).post {
                    it.onStarted()
                }
            }
        }

        override fun onIdle() {
            Log.d(LOGTAG, "Account syncing has finished")

            isSyncing = false

            services.accountManager.accountProfile()?.email?.let {
                SettingsStore.getInstance(context).setFxALastSync(it, getLastSynced(context))
            }

            syncListeners.toMutableList().forEach {
                Handler(Looper.getMainLooper()).post {
                    it.onIdle()
                }
            }
        }

        override fun onError(error: Exception?) {
            Log.d(LOGTAG, "There was an error while syncing the account: " + error?.localizedMessage)

            isSyncing = false
            syncListeners.toMutableList().forEach {
                Handler(Looper.getMainLooper()).post {
                    it.onError(error)
                }
            }
        }
    }

    private val deviceConstellationObserver = object : DeviceConstellationObserver {
        override fun onDevicesUpdate(constellation: ConstellationState) {
            Log.d(LOGTAG, "Device constellation has been updated: " + constellation.otherDevices.toString())

            otherDevices = constellation.otherDevices
            deviceConstellationListeners.toMutableList().forEach {
                Handler(Looper.getMainLooper()).post {
                    it.onDevicesUpdate(constellation)
                }
            }
        }
    }

    private val accountObserver = object : AccountObserver {
        override fun onAuthenticated(account: OAuthAccount, authType: AuthType) {
            Log.d(LOGTAG, "The user has been successfully logged in")

            if (authType !== AuthType.Existing) {
                GleanMetricsService.FxA.signInResult(true)
            }

            accountStatus = AccountStatus.SIGNED_IN

            // Enable syncing after signing in
            syncNowAsync(SyncReason.EngineChange, true)

            Handler(Looper.getMainLooper()).post {
                // Update device list
                account.deviceConstellation().registerDeviceObserver(
                    deviceConstellationObserver,
                    ProcessLifecycleOwner.get(),
                    true
                )

                refreshDevicesAsync()

                accountListeners.toMutableList().forEach {
                    it.onAuthenticated(account, authType)
                }
                originSessionId = null
            }
        }

        override fun onAuthenticationProblems() {
            Log.d(LOGTAG, "There was a problem authenticating the user")

            GleanMetricsService.FxA.signInResult(false)

            originSessionId = null

            accountStatus = AccountStatus.NEEDS_RECONNECT
            accountListeners.toMutableList().forEach {
                Handler(Looper.getMainLooper()).post {
                    it.onAuthenticationProblems()
                }
            }
        }

        override fun onLoggedOut() {
            Log.d(LOGTAG, "The user has been logged out")

            originSessionId = null

            accountStatus = AccountStatus.SIGNED_OUT
            accountListeners.toMutableList().forEach {
                Handler(Looper.getMainLooper()).post {
                    it.onLoggedOut()
                }
            }

            loadDefaultProfilePicture()
        }

        override fun onProfileUpdated(profile: Profile) {
            Log.d(LOGTAG, "The user profile has been updated")

            accountListeners.toMutableList().forEach {
                Handler(Looper.getMainLooper()).post {
                    it.onProfileUpdated(profile)
                }
            }

            loadProfilePicture(profile)
        }
    }

    init {
        services.accountManager.registerForSyncEvents(
                syncStatusObserver, ProcessLifecycleOwner.get(), false
        )
        services.accountManager.register(accountObserver)
        accountStatus = if (services.accountManager.authenticatedAccount() != null) {
            if (services.accountManager.accountNeedsReauth()) {
                AccountStatus.NEEDS_RECONNECT

            } else {
                AccountStatus.SIGNED_IN
            }

        } else {
            AccountStatus.SIGNED_OUT
        }
    }

    private fun loadProfilePicture(profile: Profile) {
        thread {
            try {
                val url = URL(profile.avatar!!.url)
                BitmapFactory.decodeStream(url.openStream())?.let {
                    val bitmap = ViewUtils.getRoundedCroppedBitmap(it)
                    profilePicture = BitmapDrawable(context.resources, bitmap)
                    BitmapCache.getInstance(context).addBitmap(PROFILE_PICTURE_TAG, bitmap)

                } ?: throw IllegalArgumentException()

            } catch (e: Exception) {
                loadDefaultProfilePicture()

            } finally {
                accountListeners.toMutableList().forEach {
                    Handler(Looper.getMainLooper()).post {
                        it.onProfileUpdated(profile)
                    }
                }
            }
        }
    }

    private fun loadDefaultProfilePicture(): BitmapDrawable? {
        BitmapFactory.decodeResource(context.resources, R.drawable.ic_icon_settings_account)?.let {
            try {
                BitmapCache.getInstance(context).addBitmap(PROFILE_PICTURE_TAG, it)
            } catch (e: NullPointerException) {
                Log.w(LOGTAG, "Bitmap is a null pointer.")
                return null
            }
            profilePicture = BitmapDrawable(context.resources, ViewUtils.getRoundedCroppedBitmap(it))
        }

        return profilePicture
    }

    fun addAccountListener(aListener: AccountObserver) {
        if (!accountListeners.contains(aListener)) {
            accountListeners.add(aListener)
        }
    }

    fun removeAccountListener(aListener: AccountObserver) {
        accountListeners.remove(aListener)
    }

    fun removeAllAccountListeners() {
        accountListeners.clear()
    }

    fun addSyncListener(aListener: SyncStatusObserver) {
        if (!syncListeners.contains(aListener)) {
            syncListeners.add(aListener)
        }
    }

    fun removeSyncListener(aListener: SyncStatusObserver) {
        syncListeners.remove(aListener)
    }

    fun removeAllSyncListeners() {
        syncListeners.clear()
    }

    fun addDeviceConstellationListener(aListener: DeviceConstellationObserver) {
        if (!deviceConstellationListeners.contains(aListener)) {
            deviceConstellationListeners.add(aListener)
        }
    }

    fun removeDeviceConstellationListener(aListener: DeviceConstellationObserver) {
        deviceConstellationListeners.remove(aListener)
    }

    fun removeAllDeviceConstellationListeners() {
        deviceConstellationListeners.clear()
    }

    fun authUrlAsync(): CompletableFuture<String?>? {
        GleanMetricsService.FxA.signIn()
        return CoroutineScope(Dispatchers.Main).future {
            services.accountManager.beginAuthentication()
        }
    }

    fun refreshDevicesAsync(): CompletableFuture<Boolean?>? {
        return CoroutineScope(Dispatchers.Main).future {
            services.accountManager.authenticatedAccount()?.deviceConstellation()?.refreshDevices()
        }
    }

    fun pollForEventsAsync(): CompletableFuture<Boolean?>? {
        return CoroutineScope(Dispatchers.Main).future {
            services.accountManager.authenticatedAccount()?.deviceConstellation()?.pollForCommands()
        }
    }

    fun updateProfileAsync(): CompletableFuture<Profile?>? {
        return CoroutineScope(Dispatchers.Main).future {
            services.accountManager.fetchProfile()
        }
    }

    fun syncNowAsync(reason: SyncReason = SyncReason.User,
                     debounce: Boolean = false): CompletableFuture<Unit?>?{
        return CoroutineScope(Dispatchers.Main).future {
            services.accountManager.syncNow(reason, debounce)
        }
    }

    fun setSyncStatus(engine: SyncEngine, value: Boolean) {
        when(engine) {
            SyncEngine.Bookmarks -> {
                GleanMetricsService.FxA.bookmarksSyncStatus(value)
            }
            SyncEngine.History -> {
                GleanMetricsService.FxA.historySyncStatus(value)
            }
        }

        syncStorage.setStatus(engine, value)
    }

    fun accountProfile(): Profile? {
        return services.accountManager.accountProfile()
    }

    fun logoutAsync(): CompletableFuture<Unit?>? {
        GleanMetricsService.FxA.signOut()

        otherDevices = emptyList()
        return CoroutineScope(Dispatchers.Main).future {
            services.accountManager.logout()
        }
    }

    fun isEngineEnabled(engine: SyncEngine): Boolean {
        return syncStorage.getStatus()[engine]?: false
    }

    fun isSignedIn(): Boolean {
        return (accountStatus == AccountStatus.SIGNED_IN)
    }

    fun lastSync(): Long {
        services.accountManager.accountProfile()?.email?.let {
            return SettingsStore.getInstance(context).getFxALastSync(it)
        }
        return 0
    }

    fun devicesByCapability(capabilities: List<DeviceCapability>): List<Device> {
        return otherDevices.filter { it.capabilities.containsAll(capabilities) }
    }

    fun sendTabs(targetDevices: List<Device>, url: String, title: String) {
        CoroutineScope(Dispatchers.Main).launch {
            services.accountManager.authenticatedAccount()?.deviceConstellation()?.let { constellation ->
                // Ignore devices that can't receive tabs or are not in the received list
                val targets = constellation.state()?.otherDevices?.filter {
                    it.capabilities.contains(DeviceCapability.SEND_TAB)
                    targetDevices.contains(it)
                }

                targets?.forEach { it ->
                    constellation.sendCommandToDevice(
                            it.id, DeviceCommandOutgoing.SendTab(title, url)
                    ).also { if (it) GleanMetricsService.FxA.sentTab() }
                }
            }
        }
    }

    fun getConnectionSuccessURL(): String {
        return (services.accountManager.authenticatedAccount() as FirefoxAccount).getConnectionSuccessURL()
    }

    fun setOrigin(origin: LoginOrigin, sessionId: String?) {
        loginOrigin = origin
        originSessionId = sessionId
    }

}